查看原文
其他

攻击TrustZone系列(Pt.2) -- 逆向高通TrustZone

2017-09-19 wx_rd.cheung 看雪学院


在这篇博客中,我们将逆向高通TrustZone的实现(SnapDragon SoC上)。

 


从哪里开始?



 

首先,因为高通TrustZone的实现是闭源的,所以据我所知,市面上没有任何关于它的架构或是设计的文档。所以,我们大概得要逆向它的二进制码来得到TrustZone的代码,并且分析它。

 


取得TrustZone的镜像文件




 

我们有两种地方来取得镜像文件;要么从设备本身,要么从设备的原厂镜像。

 

我的Nexus 5已经有了root权限,所以从设备中得到镜像十分直接。因为镜像存储在eMMC芯片中,而eMMC芯片的分区和板块可以从“/dev/block/platform/msm_sdcc.1 ”下取得,我只需要简单的将相关分区复制到我的电脑里“用dd”。

 

并且,这些分区的名字都有自己的意义:

 

 就像在图中,有两个分区,一个叫做“tz”(TrustZone的简写),一个叫做“tzb”(TrustZone backup的简写)。他们两个的内容是一样的。


然而,通过这个方式取得镜像文件,我仍然觉得不满意,两个原因:

——虽然TrustZone镜像文件存储在eMMC芯片中,但高通很简单就可以让正常世界没法得到这个镜像(要求系统主线上的AxPROT比特位必须被置位),或者这个镜像可能缺少某些部分。

——从整个分区里提取数据并不能泄露实际(逻辑上)镜像的界限,所以我们还要做些额外的工作才能确定tz镜像究竟在何处完结(在这里,因为tz镜像是一个elf文件,所以我们可以通过elf头文件简单的解决这个问题,但这只是我们运气好罢了)


所以,用完第一种方法,我们再来看看第二种方法,原厂镜像。


Nexus 5的原厂镜像可以从Google中下载。这个原厂镜像包涵一个zip压缩文件,这个zip里有全部默认的镜像,并且还有BootLoader镜像。


在下载了原厂镜像后,我们用grep来找和TrustZone有关的字符串,很快我们就能发现在BootLoader镜像中有我们想要的代码。


然而,还有一个问题。BootLoader镜像是一个未知格式(也许google工作人员知道这是啥)。即使这样,用十六进制编辑器打开这个镜像文件就能猜到大部分结构,其实这个格式很简单:




这个BootLoader镜像文件有以下结构:

——特征值(“BOOTLDR!”) - 8字节

——镜像个数 - 4字节

——从文件开始到镜像开始的偏移 - 4字节

——镜像中数据的总大小 - 4字节

——一个数列,这个数列的大小和“镜像个数”中的值相同,这个数列中每个向量有两个参数:

  ——镜像名字 - 64字节(用0来填充)

  ——镜像长度 - 4字节


你可以看到上图中叫tz的镜像就是我们要找的镜像。为了解开这个文件,我写了一个小的python脚本(在这儿),这个脚本会打开BootLoader镜像然后弄出里面的镜像。


将镜像弄出来以后,我们将它与我们用第一个方法得到的镜像相比较,结果是一样的。所以我觉得这意味我们可以接着分析TrustZone镜像咯。



修补TrustZone镜像




首先,我们发现这些镜像文件其实都是ELF文件,好消息呀!这意味着它们的内存分段和映射地址我们都应该能知道。


我们用IDA Pro打开这个文件,并且让IDA的自动分析跑了一会儿,我想开始逆向这个文件。然而,令人意外的是,好像有一大堆分支都指向没有映射的地址(甚至于,“tz”二进制文件里不包含的地址)。


我又看了一会儿,似乎所有指向没有映射的地址的分支都在第一个代码分段里,而且他们都指向没有映射的高地址。并且,没有任何绝对分支指向第一段代码分段。


这看上去有些可疑。。。所以我们来看看这个ELF文件的文件结构吧?使用readelf可以看到以下信息:




可以发现,有一个NULL类分段映射到高地址,这个分段相对应的映射地址就是那些无效绝对分支指向的地方!


所以,我做了一个安全的猜测,第一个代码分段其实映射到了错误的地址,它应该被映射到更高的地址 -- 0xfe840000. 所以自然的,我想要重定位这个分段的基地址,然而当我使用IDA的重定位基地址时,IDA崩溃了:




我实际上不确定这是高通故意来防止逆向工程的方法,还是说Null分段只是他们内部build过程的结果,但是这可以通过手动修复ELF文件来解决。我们需要做的只是将Null分段移到一个未使用过的地址(反正他也被IDA忽略了),然后再移第一个代码分段,从0x0fc86000到0xfe840000,像以下这样:




现在,再次把镜像文件用IDA读取,所有的绝对分支都有效了!这意味着我们可以接着分析镜像咯。



分析TrustZone镜像




首先,值得注意的是TrustZone镜像文件是一个相对比较大的二进制文件(285.5KB),其中的字符串相对较少,并且没有公开的文档。其次,TrustZone系统由一个完整的内核组成,这个内核具有例如执行应用程序的能力等等。所以。。。我们应该从哪里开始逆向并不明确,因为如果逆向整个二进制文件会花太多时间。


因为我们想要做的的是从应用处理器攻击TrustZone内核,所以最大的攻击面应该是那些是的正常世界可以与安全世界交互的SMC。


值得注意的是,当然,除了SMC以外,我们还有其他与TrustZone交互的方式,比如说共享内存或是中断处理,但因为这些交互方式提供的攻击面更窄,所以从分析SMC调用开始应该是个好的选择。


所以说,我们如何在TrustZone内核中找到处理SMC调用的部分?首先,让我们先回想一下当执行SMC调用时,类似于处理正常世界的系统调用(SVC调用),安全世界必须先注册当遇到这个指令时处理器将要跳转到的向量地址(向量就是指一小段代码,类似于x86的中断向量)。


安全世界的对应是MVBAR(监督向量基地址寄存器),它将提供向量地址,这个向量地址包涵安全世界处理器对不同世界的处理函数。


使用MRC/MCR操作码就可以控制MVBAR,比如以下操作码:




这意味着我们可以简单的通过在TrustZone镜像里搜索MCR操作码,来找到监督向量。事实上,在IDA里搜索操作码会得到以下结果:




如图,start符号的地址(顺便说一句,这是唯一的exported符号)被载入到了MVBAR里。


根据ARM文档,这个监督向量有以下结构:




这意味着如果我们看向之前提到的start符号,我们可以将下面的名字分配给那个表:




现在,我们就能够分析上图中的SMC_VECTOR_HANDLER函数(SMC向量处理函数)。事实上,这个函数负责了相当多的任务;首先,它保存所有的状态寄存器和返回地址到预先定义好的地址(在安全世界里),然后,它将栈切换到预先定义好的区域(也是安全世界里)。最后,在进行了必要的准备后,它接着分型用户要求的操作并且按照要求进行操作。


因为执行SMC的代码被包含在Linux内核的高通MSM分支,我们可以看看从正常世界向安全世界发送的操作要求的格式。



SMS与SCM




高通选择将安全世界与正常世界通过SMC操作码交互的通道叫做SCM(安全通道管理者)。


就像我之前博客里说的,Linux内核里的“qseecom”驱动被用于与安全世界通信,其中使用的方式就是SCM。


高通在Linux内核里提供的文档延展性很好,而且足够让我们知道SCM命令的格式。


简单的来说,SCM命令有两类:


正常SCM调用:这些调用将在需要从正常世界向安全世界传送信息时使用,它们对于处理SCM调用是必须的。内核将传播一下结构体:




然后TrustZone内核在处理完SMC条用后,将使用“scm_response”结构体来进行回复:




为了要分配和填充这些结构体,内核也许会调用包裹函数“scm_call”,这将得到指向内核空间里想要传送的数据的位置的指针,以及数据应该返回到的地址。最重要的是服务标识和命令标识。


每一个SCM调用都有一个类别,代表着哪一个TrustZone内核的子系统应该负责处理这个调用。这将由服务标识确定。命令标识确定的是,给定一个服务,哪一个命令被要求执行。


在“scm_call”函数分配和传播“smc_command”和“smc_response”缓存之后,它将调用内部“__scm_call”函数,这个函数会洗掉所有的cache(内部和外部cache),然后调用“smc”函数。


最后一个函数将实际上执行SMC操作码,将控制权交给到TrustZone内核手上,如下图:




值得注意的是R0被设为了1,R1被设为了指向本地内核栈地址,这个调用里用作“context ID”表示。R2被社为了指向分配好的“scm_command”结构体的物理地址。


这个R0里的特征值代表这是一个正常SCM调用,使用“smc_command”结构。虽然如此,对于有些不需要那么多数据的命令,不需要浪费这么多资源来分配这些个数据结构。为了解决这个问题,另一类SCM调用被介绍进来。


原子SCM调用:对于那些参数只有四个以下的命令,还有一种方式来进行SCM调用。


有四个包裹函数“scm_call_atomic_[1-4]”对应着传递的参数的个数。这些函数可以被用于使用指定服务标识命令标识以及参数来直接进行SMC调用。


以下是“scm_call_atomic1”函数:




其中SCM_ATOMIC的定义如下:




值得注意的是服务标识和命令标识被编码进了R0,传递的参数个数也在其中(这个函数里是1)。这取代了之前正常SMC调用里值为1的特征码。


这个R0里不同的值将告诉TrustZone内核接下来的SCM调用是一个原子调用,这意味着参数将由R2-R5来传递(而不是使用一个R2指向的结构体)。



分析SCM调用




既然我们理解了SCM调用是怎么工作的,并且我们已经发现了TrustZone内核里处理这些SCM函数的函数,我们可以开始反汇编SCM调用来尝试着找到任意一个SCM的漏洞。


我会略过大部分SMC处理函数的分析,因为这里面大部分都是标准的用户输入处理之类的。虽然如此,在切换到TrustZone的栈并且保存了原来世界调用时的寄存器后,处理函数继续处理服务标识和命令标识来看看哪个内部处理函数被调用了。


为了简单的完成服务标识和命令标识与处理函数的映射,一个静态的列表编译进了TrustZone镜像的数据分段,并且被SCM处理函数所引用。以下是列表的一部分:




如图,这个列表有以下结构:


——指向包含SCM函数名称的字符串的指针

——调用类型

——指向处理函数的指针

——参数个数

——每个参数的大小(DWORD)

——服务标识和命令标识,被连接进了同一个DWORD里 - 比如说,“tz_blow_sw_fuse”函数,有0x2002,这表示它的服务标识是0x20,命令标识是0x02.


现在所要做的只剩下反汇编这些函数,并且找到一个可以攻击的漏洞。



漏洞




所以在看完之前提到的SMC调用(69个)后,我终于找到了以下函数:




通常,当一个SCM命令使用正常SCM调用机制被调用,R0会含有结果地址,这个地址指向Linux内核分配的“scm_response”缓存,但同时也会被TrustZone内核验证是否这的确是一个指向允许范围内的物理地址 - 即是说,是一个指向Linux内核的内存的物理地址,而不是一个指向TrustZone内存的物理地址。


这个验证是通过一个内部函数来验证的,我会在下一篇博客中细聊。


但如果我们使用原子SMC调用来执行函数时会怎样呢?这个情况下,被使用的结果地址将是原子SMC调用的第一个参数。


现在 - 你可以看到上图函数的问题了吧?


相对于其他的SMC处理函数,这个处理函数无法验证R0里的值,即结果地址,所以让我们传入:

——一个非零数到R1(为了通过第一个分支)

——一个非零书到第四个参数(将会被传入上图的var_1C)

——物理地址到R0,包含一个在TrustZone的地址空间的物理地址


这个函数将到达上图最左边的分支,并且写一个0的DWORD到R0里包涵的地址。



责任揭秘




我想说我已经在2014年通过责任揭秘告知了高通这个漏洞,并且这个问题已经被打了补丁。我在下一个博客中将分享具体的时间历程和解释,但我想表扬一下高通工作人员,他们回复很快,并且与他们的合作很愉快。



下一步?




在下一个博客中我将会分享详细(并且复杂!)的基于刚刚那个漏洞的攻击。它将使我们能够在TrustZone内核中运行任意代码。我将分享完整的攻击码,保持关注哦!


并且,因为这是我的第二篇博客了,我想有些回复,比如说:

——我应该关注更多(或更少)哪些内容?

——博客设计的问题

——研究想法:)




本文由看雪翻译小组 wx_rd.cheung 编译

转载请注明来自看雪社区

热门阅读


点击阅读原文/read,

更多干货等着你~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存